package org.gbif.api.model.common;
import java.io.IOException;
import java.net.URI;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.validation.constraints.NotNull;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.SerializerProvider;
import org.codehaus.jackson.map.annotate.JsonDeserialize;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.codehaus.jackson.map.ser.std.SerializerBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class representing a single Digital Object Identifier (DOI) breaking it down to a prefix and suffix.
* For the syntax of DOI names see the <a href="http://www.doi.org/doi_handbook/2_Numbering.html#2.2">DOI Handbook</a>.
* All parsing is case insensitive and resulting components will all be upper cased.
*/
@JsonSerialize(using = DOI.Serializer.class)
@JsonDeserialize(using = DOI.Deserializer.class)
public class DOI {
private static final Logger LOG = LoggerFactory.getLogger(DOI.class);
/**
* The DOI prefix registered with DataCite to be used by GBIF issued production DOIs.
*/
public static final String GBIF_PREFIX = "10.15468";
/**
* A DOI prefix provided by DataCite to be used in tests.
*/
public static final String TEST_PREFIX = "10.5072";
private static final Pattern HTTP = Pattern.compile("^https?://(dx\\.)?doi\\.org/"
+ "(urn:)?(doi:)?", Pattern.CASE_INSENSITIVE);
private static final Pattern PARSER = Pattern.compile("^(?:urn:)?(?:doi:)?" // optional
+ "(10(?:\\.[0-9]+)+)"
+ "/(.+)$", Pattern.CASE_INSENSITIVE);
private static final String RESOLVER = "https://doi.org/";
private static final String SCHEME = "doi:";
private String prefix;
private String suffix;
/**
* Returns true only if the source can be parsed into a DOI.
*/
public static boolean isParsable(String source) {
if (!Strings.isNullOrEmpty(source)) {
try {
return PARSER.matcher(decodeUrl(source)).find();
}
catch (IllegalArgumentException iaEx){
LOG.debug("Can not decode URL from the following DOI: {}", source);
}
}
return false;
}
/**
* Do not use this constructor.
* Required by JAXB to marshall this object without introducing adapters.
*/
public DOI() {
}
/**
* Parses a simple DOI string of various forms incl URN, URL or plain DOI names.
* @param doi the full simple DOI string
* @throws java.lang.IllegalArgumentException if invalid DOI string is passed
*/
public DOI(String doi) {
Preconditions.checkNotNull(doi, "DOI required");
Matcher m = PARSER.matcher(decodeUrl(doi));
if (m.find()) {
this.prefix = m.group(1).toLowerCase();
this.suffix = m.group(2).toLowerCase();
} else {
throw new IllegalArgumentException(doi + " is not a valid DOI");
}
}
/**
* Parses a simple DOI string of various forms incl URN, URL or plain DOI names.
* @param prefix a simple DOI prefix starting with 10.
* @param suffix arbitrary suffix part of the DOI
* @throws java.lang.IllegalArgumentException if invalid DOI prefix is given
*/
public DOI(String prefix, String suffix) {
this.prefix = Preconditions.checkNotNull(prefix, "DOI prefix required").toLowerCase();
Preconditions.checkArgument(prefix.startsWith("10."));
this.suffix = Preconditions.checkNotNull(suffix, "DOI suffix required").toLowerCase();
}
/**
* If the doi is encoded as a URL this method strips the resolver and decodes the URL encoded string entities.
* @param doi not null doi represented as a String
* @return the path part if the doi is a URL otherwise the doi is returned as is.
* @throws IllegalArgumentException
*/
private static String decodeUrl(@NotNull String doi) {
Matcher m = HTTP.matcher(doi);
if (m.find()) {
// strip resolver incl potentially starting paths using a badly encoded urn:doi
// (the colon would need to be encoded in a proper URL)
doi = m.replaceFirst("");
// now decode the URL path, we cannot possibly have query parameters or anchors as the DOIs encoded as a URL
// will just be a path
return URI.create(doi).getPath();
}
return doi;
}
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
/**
* See <a href="http://www.doi.org/doi_handbook/2_Numbering.html#2.6">DOI Hanbook, Visual presentation and other representation of DOI names</a>.
* @return the resolved DOI using http://dx.doi.org
*/
public URI getUrl() {
return URI.create(RESOLVER + prefix + '/' + suffix);
}
/**
* @return the pure DOI name without any initial scheme name starting with the prefix, i.e. 10.
*/
public String getDoiName() {
return prefix + '/' + suffix;
}
@Override
public String toString() {
return SCHEME + prefix + '/' + suffix;
}
@Override
public int hashCode() {
return Objects.hash(prefix, suffix);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final DOI other = (DOI) obj;
// prefix and suffix are always upper cased so we can do simple equals here
return Objects.equals(this.prefix, other.prefix) && Objects.equals(this.suffix, other.suffix);
}
/**
* Serializes a DOI as doi name with a doi: scheme.
* For example doi:10.1038/nature.2014.16460
*/
public static class Serializer extends SerializerBase<DOI> {
public Serializer() {
super(DOI.class);
}
@Override
public void serialize(DOI value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(value.toString());
}
}
/**
* Deserializes a DOI from various string based formats.
* See DOI constructor for details.
*/
public static class Deserializer extends JsonDeserializer<DOI> {
@Override
public DOI deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
if (jp != null && jp.getTextLength() > 0) {
return new DOI(jp.getText());
}
return null;
}
}
}